package com.example.glideprogressiveloadimg.util;

import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

import static com.example.glideprogressiveloadimg.util.Preconditions.checkNotNull;

public class ByteStreams {


    private static final int BUF_SIZE = 0x1000; // 4K

    private ByteStreams() {}

    /**
     * Copies all bytes from the input stream to the output stream. Does not close or flush either
     * stream.
     *
     * @param from the input stream to read from
     * @param to the output stream to write to
     * @return the number of bytes copied
     * @throws IOException if an I/O error occurs
     */
    public static long copy(InputStream from, OutputStream to) throws IOException {
        checkNotNull(from);
        checkNotNull(to);
        byte[] buf = new byte[BUF_SIZE];
        long total = 0;
        while (true) {
            int r = from.read(buf);
            if (r == -1) {
                break;
            }
            to.write(buf, 0, r);
            total += r;
        }
        return total;
    }

    /**
     * Reads some bytes from an input stream and stores them into the buffer array {@code b}. This
     * method blocks until {@code len} bytes of input data have been read into the array, or end of
     * file is detected. The number of bytes read is returned, possibly zero. Does not close the
     * stream.
     *
     * <p>A caller can detect EOF if the number of bytes read is less than {@code len}. All subsequent
     * calls on the same stream will return zero.
     *
     * <p>If {@code b} is null, a {@code NullPointerException} is thrown. If {@code off} is negative,
     * or {@code len} is negative, or {@code off+len} is greater than the length of the array {@code
     * b}, then an {@code IndexOutOfBoundsException} is thrown. If {@code len} is zero, then no bytes
     * are read. Otherwise, the first byte read is stored into element {@code b[off]}, the next one
     * into {@code b[off+1]}, and so on. The number of bytes read is, at most, equal to {@code len}.
     *
     * @param in the input stream to read from
     * @param b the buffer into which the data is read
     * @param off an int specifying the offset into the data
     * @param len an int specifying the number of bytes to read
     * @return the number of bytes read
     * @throws IOException if an I/O error occurs
     */
    public static int read(InputStream in, byte[] b, int off, int len) throws IOException {
        checkNotNull(in);
        checkNotNull(b);
        if (len < 0) {
            throw new IndexOutOfBoundsException("len is negative");
        }
        int total = 0;
        while (total < len) {
            int result = in.read(b, off + total, len - total);
            if (result == -1) {
                break;
            }
            total += result;
        }
        return total;
    }

    /**
     * Reads all bytes from an input stream into a byte array. Does not close the stream.
     *
     * @param in the input stream to read from
     * @return a byte array containing all the bytes from the stream
     * @throws IOException if an I/O error occurs
     */
    public static byte[] toByteArray(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        copy(in, out);
        return out.toByteArray();
    }

    /**
     * Reads all bytes from an input stream into a byte array. The given expected size is used to
     * create an initial byte array, but if the actual number of bytes read from the stream differs,
     * the correct result will be returned anyway.
     */
    public static byte[] toByteArray(InputStream in, int expectedSize) throws IOException {
        byte[] bytes = new byte[expectedSize];
        int remaining = expectedSize;

        while (remaining > 0) {
            int off = expectedSize - remaining;
            int read = in.read(bytes, off, remaining);
            if (read == -1) {
                // end of stream before reading expectedSize bytes
                // just return the bytes read so far
                return Arrays.copyOf(bytes, off);
            }
            remaining -= read;
        }

        // bytes is now full
        int b = in.read();
        if (b == -1) {
            return bytes;
        }

        // the stream was longer, so read the rest normally
        FastByteArrayOutputStream out = new FastByteArrayOutputStream();
        out.write(b); // write the byte we read when testing for end of stream
        copy(in, out);

        byte[] result = new byte[bytes.length + out.size()];
        System.arraycopy(bytes, 0, result, 0, bytes.length);
        out.writeTo(result, bytes.length);
        return result;
    }

    /** BAOS that provides limited access to its internal byte array. */
    private static final class FastByteArrayOutputStream extends ByteArrayOutputStream {
        /**
         * Writes the contents of the internal buffer to the given array starting at the given offset.
         * Assumes the array has space to hold count bytes.
         */
        void writeTo(byte[] b, int off) {
            System.arraycopy(buf, 0, b, off, count);
        }
    }

    /**
     * Attempts to read {@code len} bytes from the stream into the given array starting at {@code
     * off}, with the same behavior as {@link DataInput#readFully(byte[], int, int)}. Does not close
     * the stream.
     *
     * @param in the input stream to read from.
     * @param b the buffer into which the data is read.
     * @param off an int specifying the offset into the data.
     * @param len an int specifying the number of bytes to read.
     * @throws EOFException if this stream reaches the end before reading all the bytes.
     * @throws IOException if an I/O error occurs.
     */
    public static void readFully(InputStream in, byte[] b, int off, int len) throws IOException {
        int read = read(in, b, off, len);
        if (read != len) {
            throw new EOFException(
                    "reached end of stream after reading " + read + " bytes; " + len + " bytes expected");
        }
    }
}
